/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import Unsafe from './Unsafe';
import { CompressionParameters } from './CompressionParameters';
import RepeatedOffsets from './RepeatedOffsets';
import BlockCompressionState from './BlockCompressionState';
import SequenceStore from './SequenceStore';
import Constants from './Constants'
import { BlockCompressor } from './BlockCompressor'
import Long from '../util/long/index'
import Exception from '../util/Exception';

export default class DoubleFastBlockCompressor implements BlockCompressor {
    private static MIN_MATCH: number = 3;
    private static SEARCH_STRENGTH: number = 8;
    private static REP_MOVE: number = Constants.REPEATED_OFFSET_COUNT - 1;

    public compressBlock(inputBase: any, inputAddress: Long, inputSize: number, output: SequenceStore, state: BlockCompressionState, offsets: RepeatedOffsets, parameters: CompressionParameters): number
    {
        let matchSearchLength: number = Math.max(parameters.getSearchLength(), 4);

        let baseAddress: Long = state.getBaseAddress();
        let windowBaseAddress: Long = baseAddress.add(state.getWindowBaseOffset());

        let longHashTable: Int32Array = state.hashTable;
        let longHashBits: number = parameters.getHashLog();

        let shortHashTable: Int32Array = state.chainTable;
        let shortHashBits: number = parameters.getChainLog();

        let inputEnd: Long = inputAddress.add(inputSize);
        let inputLimit: Long = inputEnd.sub(Constants.SIZE_OF_LONG); // We read a long at a time for computing the hashes

        let input: Long = inputAddress;
        let anchor: Long = inputAddress;

        let offset1: number = offsets.getOffset0();
        let offset2: number = offsets.getOffset1();

        let savedOffset: number = 0;

        if (input.sub(windowBaseAddress).eq(0)) {
            input = input.add(1);
        }
        let maxRep: number = input.sub(windowBaseAddress).toInt();

        if (offset2 > maxRep) {
            savedOffset = offset2;
            offset2 = 0;
        }

        if (offset1 > maxRep) {
            savedOffset = offset1;
            offset1 = 0;
        }

        while (input.lessThan(inputLimit)) { // < instead of <=, because repcode check at (input+1)
            let shortHash: number = DoubleFastBlockCompressor.hash(inputBase, input, shortHashBits, matchSearchLength);
            let shortMatchAddress: Long = baseAddress.add(shortHashTable[shortHash]);

            let longHash: number = DoubleFastBlockCompressor.hash8(Unsafe.getLong(inputBase, input.toNumber()), longHashBits);
            let longMatchAddress: Long = baseAddress.add(longHashTable[longHash]);

            let current: number = input.sub(baseAddress).toInt();
            longHashTable[longHash] = current;
            shortHashTable[shortHash] = current;

            let matchLength: number;
            let offset: number;

            if (offset1 > 0 && Unsafe.getInt(inputBase, input.add(1).sub(offset1).toNumber())
            == Unsafe.getInt(inputBase, input.add(1).toNumber())) {
                matchLength = DoubleFastBlockCompressor.count(inputBase, input.add(1)
                    .add(Constants.SIZE_OF_INT), inputEnd, input.add(1)
                    .add(Constants.SIZE_OF_INT)
                    .sub(offset1)) + Constants.SIZE_OF_INT;
                input = input.add(1);
                output.storeSequence(inputBase, anchor, input.sub(anchor)
                    .toInt(), 0, matchLength - DoubleFastBlockCompressor.MIN_MATCH);
            } else {
                // check prefix long match
                if (longMatchAddress.greaterThan(windowBaseAddress) &&
                (Unsafe.getLong(inputBase, longMatchAddress.toNumber())
                    .eq(Unsafe.getLong(inputBase, input.toNumber())))) {
                    matchLength = DoubleFastBlockCompressor.count(inputBase, input.add(Constants.SIZE_OF_LONG), inputEnd,
                    longMatchAddress.add(Constants.SIZE_OF_LONG)) + Constants.SIZE_OF_LONG;
                    offset = input.sub(longMatchAddress).toInt();
                    while (input.greaterThan(anchor) && longMatchAddress.greaterThan(windowBaseAddress) && Unsafe.getByte(inputBase,
                    input.sub(1)
                        .toNumber()) == Unsafe.getByte(inputBase, longMatchAddress.sub(1).toNumber())) {
                        input = input.sub(1);
                        longMatchAddress = longMatchAddress.sub(1);
                        matchLength++;
                    }
                } else {
                    if (shortMatchAddress.greaterThan(windowBaseAddress)
                    && (Unsafe.getInt(inputBase, shortMatchAddress.toNumber())
                    == Unsafe.getInt(inputBase, input.toNumber()))) {
                        let nextOffsetHash: number = DoubleFastBlockCompressor.hash8(Unsafe.getLong(inputBase, input.add(1)
                            .toNumber()), longHashBits);
                        let nextOffsetMatchAddress: Long = baseAddress.add(longHashTable[nextOffsetHash]);
                        longHashTable[nextOffsetHash] = current + 1;

                        // check prefix long +1 match
                        if (nextOffsetMatchAddress.greaterThan(windowBaseAddress) &&
                        (Unsafe.getLong(inputBase, nextOffsetMatchAddress.toNumber())
                            .eq(Unsafe.getLong(inputBase, input.add(1)
                                .toNumber())))) {

                            matchLength = DoubleFastBlockCompressor.count(inputBase, input.add(1)
                                .add(Constants.SIZE_OF_LONG), inputEnd, nextOffsetMatchAddress.add(Constants.SIZE_OF_LONG))
                            + Constants.SIZE_OF_LONG;
                            input = input.add(1);
                            offset = input.sub(nextOffsetMatchAddress).toInt();

                            while (input.greaterThan(anchor) && nextOffsetMatchAddress.greaterThan(windowBaseAddress)
                            && Unsafe.getByte(inputBase, input.sub(1).toNumber())
                            == Unsafe.getByte(inputBase, nextOffsetMatchAddress.sub(1).toNumber())) {

                                input = input.sub(1);
                                nextOffsetMatchAddress = nextOffsetMatchAddress.sub(1);
                                matchLength++;
                            }
                        } else {
                            // if no long +1 match, explore the short match we found
                            matchLength = DoubleFastBlockCompressor.count(inputBase, input.add(Constants.SIZE_OF_INT), inputEnd, shortMatchAddress.add(Constants.SIZE_OF_INT)) + Constants.SIZE_OF_INT;
                            offset = input.sub(shortMatchAddress).toInt();
                            while (input.greaterThan(anchor) && shortMatchAddress.greaterThan(windowBaseAddress)
                            && (Unsafe.getByte(inputBase, input.sub(1).toNumber())
                            == Unsafe.getByte(inputBase, shortMatchAddress.sub(1).toNumber()))) {
                                input = input.sub(1);
                                shortMatchAddress = shortMatchAddress.sub(1);
                                matchLength++;
                            }
                        }
                    }
                    else {
                        input = input.add((input.sub(anchor).shiftRight(DoubleFastBlockCompressor.SEARCH_STRENGTH)).add(1));
                        continue;
                    }
                }

                offset2 = offset1;
                offset1 = offset;

                output.storeSequence(inputBase, anchor, input.sub(anchor).toInt(),
                    offset + DoubleFastBlockCompressor.REP_MOVE,
                    matchLength - DoubleFastBlockCompressor.MIN_MATCH);
            }

            input = input.add(matchLength);
            anchor = input;

            if (input.lessThanOrEqual(inputLimit)) {
                // Fill Table
                longHashTable[
                DoubleFastBlockCompressor.hash8(Unsafe.getLong(inputBase, baseAddress.add(current)
                    .add(2).toNumber()), longHashBits)] = current + 2;
                shortHashTable[
                DoubleFastBlockCompressor.hash(inputBase, baseAddress.add(current)
                    .add(2), shortHashBits, matchSearchLength)] = current + 2;

                longHashTable[
                DoubleFastBlockCompressor.hash8(Unsafe.getLong(inputBase, input.sub(2)
                    .toNumber()), longHashBits)] = input.sub(2)
                    .sub(baseAddress)
                    .toInt();
                shortHashTable[
                DoubleFastBlockCompressor.hash(inputBase, input.sub(2), shortHashBits, matchSearchLength)] = input.sub(2)
                    .sub(baseAddress)
                    .toInt();

                while (input.lessThanOrEqual(inputLimit) &&
                offset2 > 0 && (Unsafe.getInt(inputBase, input.toNumber())
                == Unsafe.getInt(inputBase, input.sub(offset2).toNumber()))) {
                    let repetitionLength: number = DoubleFastBlockCompressor.count(inputBase,
                    input.add(Constants.SIZE_OF_INT),
                        inputEnd, input.add(Constants.SIZE_OF_INT).sub(offset2))
                    + Constants.SIZE_OF_INT;

                    // swap offset2 <=> offset1
                    let temp: number = offset2;
                    offset2 = offset1;
                    offset1 = temp;

                    shortHashTable[
                    DoubleFastBlockCompressor.hash(inputBase, input, shortHashBits, matchSearchLength)] = input.sub(baseAddress)
                        .toInt();
                    longHashTable[
                    DoubleFastBlockCompressor.hash8(Unsafe.getLong(inputBase, input.toNumber()), longHashBits)] =
                    input.sub(baseAddress)
                        .toInt();

                    output.storeSequence(inputBase, anchor, 0, 0, repetitionLength - DoubleFastBlockCompressor.MIN_MATCH);

                    input = input.add(repetitionLength);
                    anchor = input;
                }
            }
        }

        // save reps for next block
        offsets.saveOffset0(offset1 != 0 ? offset1 : savedOffset);
        offsets.saveOffset1(offset2 != 0 ? offset2 : savedOffset);

        // return the last literals size
        return inputEnd.sub(anchor).toInt();
    }

    public static count(inputBase: any, inputAddress: Long, inputLimit: Long, matchAddress: Long): number
    {
        let input: Long = inputAddress;
        let match: Long = matchAddress;

        let remaining: number = inputLimit.sub(inputAddress).toInt();

        // first, compare long at a time
        let count: number = 0;
        while (count < remaining - (Constants.SIZE_OF_LONG - 1)) {
            try {
                let diff: Long = Unsafe.getLong(inputBase, match.toNumber()).xor(Unsafe.getLong(inputBase, input.toNumber()));

                if (!diff.eq(0)) {
                    return count + (DoubleFastBlockCompressor.numberOfTrailingZeros(diff) >> 3);
                }
            } catch (e) {
                throw new Exception(e);
            }

            count += Constants.SIZE_OF_LONG;
            input = input.add(Constants.SIZE_OF_LONG);
            match = match.add(Constants.SIZE_OF_LONG);
        }

        while (count < remaining && Unsafe.getByte(inputBase, match.toNumber()) == Unsafe.getByte(inputBase, input.toNumber())) {
            count++;
            input = input.add(1);
            match = match.add(1);
        }

        return count;
    }

    public static numberOfTrailingZeros(i: Long): number {
        // HD, Figure 5-14
        let x;
        let y;
        if (i.eq(0)) return 64;
        let n = 63;
        y = i.toInt();
        if (y != 0) {
            n = n - 32;
            x = y;
        } else x = i.shiftRightUnsigned(32).toInt();
        y = x << 16;
        if (y != 0) {
            n = n - 16;
            x = y;
        }
        y = x << 8;
        if (y != 0) {
            n = n - 8;
            x = y;
        }
        y = x << 4;
        if (y != 0) {
            n = n - 4;
            x = y;
        }
        y = x << 2;
        if (y != 0) {
            n = n - 2;
            x = y;
        }
        return n - ((x << 1) >>> 31);
    }

    private static hash(inputBase: any, inputAddress: Long, bits: number, matchSearchLength: number): number
    {
        switch (matchSearchLength) {
            case 8:
                return DoubleFastBlockCompressor.hash8(Unsafe.getLong(inputBase, inputAddress.toNumber()), bits);
            case 7:
                return DoubleFastBlockCompressor.hash7(Unsafe.getLong(inputBase, inputAddress.toNumber()), bits);
            case 6:
                return DoubleFastBlockCompressor.hash6(Unsafe.getLong(inputBase, inputAddress.toNumber()), bits);
            case 5:
                return DoubleFastBlockCompressor.hash5(Unsafe.getLong(inputBase, inputAddress.toNumber()), bits);
            default:
                return DoubleFastBlockCompressor.hash4(Unsafe.getInt(inputBase, inputAddress.toNumber()), bits);
        }
    }

    private static PRIME_4_BYTES: number = Long.fromString('-1640531535').toInt();
    private static PRIME_5_BYTES: Long = Long.fromString('889523592379');
    private static PRIME_6_BYTES: Long = Long.fromString('227718039650203');
    private static PRIME_7_BYTES: Long = Long.fromString('58295818150454627');
    private static PRIME_8_BYTES: Long = Long.fromString('-3523014627327384477');

    private static hash4(value: number, bits: number): number
    {
        return (value * DoubleFastBlockCompressor.PRIME_4_BYTES) >>> (32 - bits);
    }

    private static hash5(value: Long, bits: number): number
    {
        return (value.shiftLeft(64 - 40)).multiply(DoubleFastBlockCompressor.PRIME_5_BYTES)
            .shiftRightUnsigned(64 - bits)
            .toInt();
    }

    private static hash6(value: Long, bits: number): number
    {
        return ((value.shiftLeft(64 - 48)).multiply(DoubleFastBlockCompressor.PRIME_6_BYTES)).shiftRightUnsigned(64 - bits)
            .toInt();
    }

    private static hash7(value: Long, bits: number): number
    {
        return ((value.shiftLeft(64 - 56)).multiply(DoubleFastBlockCompressor.PRIME_7_BYTES)).shiftRightUnsigned(64 - bits)
            .toInt();
    }

    private static hash8(value: Long, bits: number): number
    {
        return (value.multiply(DoubleFastBlockCompressor.PRIME_8_BYTES)).shiftRightUnsigned(64 - bits).toInt();
    }
}
